package com.ghbank.rdws.common.db;

import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.sql.DataSource;

import org.springframework.stereotype.Component;

import com.alibaba.druid.pool.DruidDataSource;
import com.ghbank.rdws.common.db.utils.DataSourceTerminationTask;
import com.ghbank.rdws.common.exception.DatabaseException;
import com.ghbank.rdws.common.lifecycle.AbstractLifeCycle;
import com.ghbank.rdws.common.lifecycle.LifeCycle;
import com.ghbank.rdws.common.model.DataSourceConfig;
import com.ghbank.rdws.common.model.DbType;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import lombok.extern.slf4j.Slf4j;

/**
 * 
 * @author liuxudong
 * @date 2019年3月21日
 */
@Slf4j
@Component
public class DataSourceFactory extends AbstractLifeCycle implements LifeCycle {

    private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    private int connectionTimeout = 5 * 1000;
    private int minIdle = 1;
    private int maximumPoolSize = 32;

    private LoadingCache<DataSourceConfig, DataSource> dataSources;

    public void start() {
        super.start();
        dataSources = CacheBuilder.newBuilder().build(new CacheLoader<DataSourceConfig, DataSource>() {
            @Override
            public DataSource load(DataSourceConfig config) throws Exception {
                return createDataSource(config.getUrl(), config.getUsername(), config.getPassword(), config.getType(),
                    config.getProperties());
            }
        });

    }

    public void stop() {
        super.stop();

        ConcurrentMap<DataSourceConfig, DataSource> dataSourceMap = dataSources.asMap();
        for (DataSource source : dataSourceMap.values()) {
            DruidDataSource basicDataSource = (DruidDataSource)source;
            basicDataSource.close();
        }
        dataSources.cleanUp();
    }

    public DataSource getDataSource(DataSourceConfig config) throws ExecutionException {
        return dataSources.get(config);
    }

    public DataSource getDataSource(String url, String userName, String password, DbType dbType, Properties props)
        throws ExecutionException {
        return dataSources.get(new DataSourceConfig(url, userName, password, dbType, props));
    }

    private void asyncTerminate(DataSource dataSource) {
        DataSourceTerminationTask task = new DataSourceTerminationTask(dataSource, scheduledExecutorService);
        // start now
        scheduledExecutorService.schedule(task, 0, TimeUnit.MILLISECONDS);
    }

    public void removeDataSource(DataSourceConfig config) throws ExecutionException {
        DataSource dataSource = dataSources.getIfPresent(config);
        if (dataSource != null) {
            dataSources.invalidate(config);
            asyncTerminate(dataSource);
        }
    }

    private DataSource createDataSource(String url, String userName, String password, DbType dbType, Properties props) {
        try {
            int maximumPoolSize =
                Integer.valueOf(props.getProperty("maximumPoolSize", String.valueOf(this.maximumPoolSize)));
            if (maximumPoolSize < 0) {
                maximumPoolSize = 300;
            }
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setUrl(url);
            dataSource.setUsername(userName);
            dataSource.setPassword(password);
            dataSource.setInitialSize(minIdle);
            dataSource.setMinIdle(minIdle);
            dataSource.setMaxActive(maximumPoolSize);
            dataSource.setMaxWait(connectionTimeout);
            dataSource.setDriverClassName(dbType.getDriver());
            // 动态的参数
            if (props != null && props.size() > 0) {
                for (Map.Entry<Object, Object> entry : props.entrySet()) {
                    dataSource.addConnectionProperty(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
                }
            }
            if (dbType.isOracle()) {
                dataSource.addConnectionProperty("restrictGetTables", "true");
                dataSource.addConnectionProperty("oracle.jdbc.V8Compatible", "true");
                dataSource.setValidationQuery("select 1 from dual");
                dataSource.setExceptionSorter("com.alibaba.druid.pool.vendor.OracleExceptionSorter");
            } else if (dbType.isMysql()) {
                dataSource.addConnectionProperty("useServerPrepStmts", "false");
                dataSource.addConnectionProperty("rewriteBatchedStatements", "true");
                dataSource.addConnectionProperty("allowMultiQueries", "true");
                dataSource.addConnectionProperty("readOnlyPropagatesToServer", "false");
                dataSource.setValidationQuery("select 1");
                dataSource.setExceptionSorter("com.alibaba.druid.pool.vendor.MySqlExceptionSorter");
                dataSource
                    .setValidConnectionCheckerClassName("com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker");
            } else {
                log.error("Unknow database type");
            }
            return dataSource;
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
            throw new DatabaseException("create dataSource error!", e);
        }
    }

    public int getConnectionTimeout() {
        return connectionTimeout;
    }

    public void setConnectionTimeout(int connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    public int getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }

    public int getMaximumPoolSize() {
        return maximumPoolSize;
    }

    public void setMaximumPoolSize(int maximumPoolSize) {
        this.maximumPoolSize = maximumPoolSize;
    }

    @PostConstruct
    public void initialize() {
        if (this.isStop()) {
            this.start();
        }
    }

    @PreDestroy
    public void destory() {
        log.debug("start to execute destory method");
        if (this.isStart()) {
            this.stop();
        }
        log.debug("finish to execute destory method");
    }

}
